/****************************************************************************
    
    This file is a module for processing images. It is a part of the
    Online Editor - a simple web-application for image editing.
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program in the file "LICENSE". If not, see
    <http://www.gnu.org/copyleft/gpl.html>.
    
    Copyright (c) 2011 Murashko Alexander <alexmurka2012@gmail.com>
    
****************************************************************************/

package services;

import java.awt.image.BufferedImage;

public class ImageFunctions {
    
    /**
     * Функция изменения яркости изображения.
     * Рекомендуемый диапазон значений от -100 до +100.
     * 
     * @param image - входное изображение;
     * @param value - количество градаций яркости.
     * @return Изображение-результат типа BufferedImage.
     */
    public static BufferedImage changeBrightness(BufferedImage image, int value) {
        int w = image.getWidth();
        int h = image.getHeight();
        int[] pixels = new int[w * h];
        image.getRGB(0, 0, w, h, pixels, 0, w);
        for (int i = 0; i < w * h; i++) {
            int pixel = pixels[i];
            int r = (int) (0xff & (pixel >> 16)) + value;
            int g = (int) (0xff & (pixel >> 8)) + value;
            int b = (int) (0xff & (pixel)) + value;
            if (r < 0) r = 0; else if (r > 255) r = 255;
            if (g < 0) g = 0; else if (g > 255) g = 255;
            if (b < 0) b = 0; else if (b > 255) b = 255;
            pixels[i] = (r << 16) | (g << 8) | b;
        }
        BufferedImage resultImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        resultImage.setRGB(0, 0, w, h, pixels, 0, w);
        return resultImage;
    }
    
    /**
     * Функция изменения контрастности изображения.
     * Рекомендуемый диапазон значений от 0 до 200.
     * 
     * @param image - входное изображение;
     * @param value - степень изменения контрастности в %;
     * @return Изображение-результат типа BufferedImage.
     */
    public static BufferedImage changeContrast(BufferedImage image, int value) {
        int w = image.getWidth();
        int h = image.getHeight();
        int area = w * h;
        int[] pixels = new int[area];
        image.getRGB(0, 0, w, h, pixels, 0, w);
        //
        int avg_r = 0;
        int avg_g = 0;
        int avg_b = 0;
        for (int i = 0; i < area; i++) {
            int pixel = pixels[i];
            avg_r += 0xff & (pixel >> 16);
            avg_g += 0xff & (pixel >> 8);
            avg_b += 0xff & (pixel);
        }
        avg_r /= area;
        avg_g /= area;
        avg_b /= area;
        //
        for (int i = 0; i < area; i++) {
            int pixel = pixels[i];
            int r = ((int) (0xff & (pixel >> 16)) - avg_r) * value / 100 + avg_r;
            int g = ((int) (0xff & (pixel >> 8)) - avg_g) * value / 100 + avg_g;
            int b = ((int) (0xff & (pixel)) - avg_b) * value / 100 + avg_b;
            if (r < 0) r = 0; else if (r > 255) r = 255;
            if (g < 0) g = 0; else if (g > 255) g = 255;
            if (b < 0) b = 0; else if (b > 255) b = 255;
            pixels[i] = (r << 16) | (g << 8) | b;
        }
        //
        BufferedImage resultImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        resultImage.setRGB(0, 0, w, h, pixels, 0, w);
        return resultImage;
    }
    
    /**
     * Функция вращения изображения.
     * 
     * @param image - входное изображение;
     * @param angle - угол поворота изображения; допустимые значения: 90, -90, 180.
     * @return Изображение-результат типа BufferedImage.
     */
    public static BufferedImage rotate(BufferedImage image, int angle) {
    	int w = image.getWidth();
        int h = image.getHeight();
        int area = w * h;
        int[] pixels = new int[area];
        image.getRGB(0, 0, w, h, pixels, 0, w);
        BufferedImage resultImage = null;
        if (angle == 90) {
        	int[] newPixels =  new int[area];
        	int counter = 0;
            for (int _x = 0; _x < w; _x++)
            {
                for (int _y = h-1; _y >= 0; _y--) {
                	newPixels[counter++] = pixels[_y * w + _x];
                }
            }
            resultImage = new BufferedImage(h, w, BufferedImage.TYPE_INT_RGB);
            resultImage.setRGB(0, 0, h, w, newPixels, 0, h);
        } else if (angle == -90) {
        	int[] newPixels =  new int[area];
        	int counter = 0;
            for (int _x = w-1; _x >= 0; _x--)
            {
                for (int _y = 0; _y < h; _y++) {
                	newPixels[counter++] = pixels[_y * w + _x];
                }
            }
            resultImage = new BufferedImage(h, w, BufferedImage.TYPE_INT_RGB);
            resultImage.setRGB(0, 0, h, w, newPixels, 0, h);
        } else if (angle == 180) {
        	int[] newPixels =  new int[area];
        	int counter = 0;
            for (int _y = h-1; _y >= 0; _y--)
            {
                for (int _x = w-1; _x >= 0; _x--) {
                	newPixels[counter++] = pixels[_y * w + _x];
                }
            }
            resultImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
            resultImage.setRGB(0, 0, w, h, newPixels, 0, w);
        }
        return resultImage;
    }
    
    /**
     * Функция отражения изображения.
     * 
     * @param image - входное изображение;
     * @param direction - направление отражения изображения; допустимые значения: "horizontal", "vertical". 
     * @return Изображение-результат типа BufferedImage.
     */
    public static BufferedImage reflect(BufferedImage image, String direction) {
    	int w = image.getWidth();
        int h = image.getHeight();
        int area = w * h;
        int[] pixels = new int[area];
        image.getRGB(0, 0, w, h, pixels, 0, w);
        BufferedImage resultImage = null;
        if (direction.equals("horizontal")) {
        	int[] newPixels =  new int[area];
        	int counter = 0;
            for (int _y = 0; _y < h; _y++)
            {
                for (int _x = w-1; _x >= 0; _x--) {
                	newPixels[counter++] = pixels[_y * w + _x];
                }
            }
            resultImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
            resultImage.setRGB(0, 0, w, h, newPixels, 0, w);
        } else if (direction.equals("vertical")) {
        	int[] newPixels =  new int[area];
        	int counter = 0;
            for (int _y = h-1; _y >= 0; _y--)
            {
                for (int _x = 0; _x < w; _x++) {
                	newPixels[counter++] = pixels[_y * w + _x];
                }
            }
            resultImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
            resultImage.setRGB(0, 0, w, h, newPixels, 0, w);
        }
        return resultImage;
    }

    /**
     * Функция кадрирования (обрезки) изображения.
     * В качетве параметров принимает координаты двух противолежащих
     * точек прямоугольной области обрезки.
     * 
     * @param image - входное изображение;
     * @param firstPointX - x-координата первой точки области;
     * @param firstPointY - y-координата первой точки области;
     * @param secondPointX - x-координата второй точки области;
     * @param secondPointY - y-координата второй точки области.
     * @return Изображение-результат типа BufferedImage.
     */
    public static BufferedImage crop(BufferedImage image, int firstPointX, int firstPointY, int secondPointX, int secondPointY) {
    	if (firstPointX == secondPointX || firstPointY == secondPointY) return null;
    	int x1, x2, y1, y2;
    	if (firstPointX < secondPointX) {
    		x1 = firstPointX;
    		x2 = secondPointX;
    	} else {
    		x1 = secondPointX;
    		x2 = firstPointX;
    	}
    	if (firstPointY < secondPointY) {
    		y1 = firstPointY;
    		y2 = secondPointY;
    	} else {
    		y1 = secondPointY;
    		y2 = firstPointY;
    	}
    	//
    	int w = image.getWidth();
        int h = image.getHeight();
        int area = w * h;
        int[] pixels = new int[area];
        image.getRGB(0, 0, w, h, pixels, 0, w);
        //
        int newWidth = x2 - x1;
        int newHeight = y2 - y1;
        int[] newPixels = new int[newWidth * newHeight];
        int counter = 0;
        for (int _y = y1; _y < y2; _y++)
        {
            for (int _x = x1; _x < x2; _x++) {
            	newPixels[counter++] = pixels[_y * w + _x];
            }
        }
        BufferedImage resultImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
        resultImage.setRGB(0, 0, newWidth, newHeight, newPixels, 0, newWidth);
        return resultImage;
    }
    
    /**
     * Функция преобразования полноцветного изображения в градации серого.
     * 
     * @param image - входное изображение.
     * @return Изображение-результат типа BufferedImage.
     */
    public static BufferedImage toGrayscale(BufferedImage image) {
        int w = image.getWidth();
        int h = image.getHeight();
        int[] pixels = new int[w * h];
        image.getRGB(0, 0, w, h, pixels, 0, w);
        for (int i = 0; i < w * h; i++) {
            int pixel = pixels[i];
            int r = 0xff & (pixel >> 16);
            int g = 0xff & (pixel >> 8);
            int b = 0xff & (pixel);
            int y = (r * 77 + g * 150 + b * 29) / 256;
            pixels[i] = (y << 16) | (y << 8) | y;
        }
        BufferedImage resultImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        resultImage.setRGB(0, 0, w, h, pixels, 0, w);
        return resultImage;
    }
    
    /**
     * Функция инвертирования цветов изображения.
     * 
     * @param image - входное изображение.
     * @return Изображение-результат типа BufferedImage.
     */
    public static BufferedImage invertColors(BufferedImage image) {
        int w = image.getWidth();
        int h = image.getHeight();
        int[] pixels = new int[w * h];
        image.getRGB(0, 0, w, h, pixels, 0, w);
        for (int i = 0; i < w * h; i++) {
        	pixels[i] = 0xffffff - pixels[i];
        }
        BufferedImage resultImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        resultImage.setRGB(0, 0, w, h, pixels, 0, w);
        return resultImage;
    }


    //-------------------------------------------------------------------------//
    //--- Seamless Cloning ----------------------------------------------------//
    //-------------------------------------------------------------------------//


    /**
     * Константа, характеризующая степень точности результата, получаемого
     * при бесшовном копировании.
     */
    private static final float EPSILON = 0.001F;
    
    /**
     * Функция бесшовного копирования для полноцветных изображений.
     * Предполагается, что копируемый фрагмент имеет прямоугольную форму.
     * 
     * @param sourceImage - изображение-источник;
     * @param destinationImage - изображение-приемник;
     * @param firstPointX - координата по горизонтали верхнего левого угла копируемого фрагмента в изображении-источнике;
     * @param firstPointY - координата по вертикали верхнего левого угла копируемого фрагмента в изображении-источнике;
     * @param fragmentWidth - ширина копируемого фрагмента;
     * @param fragmentHeight - высота копируемого фрагмента;
     * @param bindingPointX - x-координата точки привязки фрагмента в изображении-приемнике;
     * @param bindingPointY - y-координата точки привязки фрагмента в изображении-приемнике.
     * @return Изображение-результат типа BufferedImage.
     */
    public static BufferedImage seamlessCloning(BufferedImage sourceImage,
                                                BufferedImage destinationImage,
                                                int firstPointX,
                                                int firstPointY,
                                                int fragmentWidth,
                                                int fragmentHeight,
                                                int bindingPointX,
                                                int bindingPointY) {
        //
        int sourceImageWidth = sourceImage.getWidth();
        int sourceImageHeight = sourceImage.getHeight();
        int sourceImageArea = sourceImageWidth * sourceImageHeight;
        int sourceImageR[] = new int [sourceImageArea];
        int sourceImageG[] = new int [sourceImageArea];
        int sourceImageB[] = new int [sourceImageArea];
        int sourceImagePixels[] = new int [sourceImageArea];
        sourceImage.getRGB(0, 0, sourceImageWidth, sourceImageHeight, sourceImagePixels, 0, sourceImageWidth);
        for (int i = 0; i < sourceImageArea; i++) {
            int pixel = sourceImagePixels[i];
            sourceImageR[i] = 0xff & (pixel >> 16);
            sourceImageG[i] = 0xff & (pixel >> 8);
            sourceImageB[i] = 0xff & (pixel);
        }
        //
        int destinationImageWidth = destinationImage.getWidth();
        int destinationImageHeight = destinationImage.getHeight();
        int destinationImageArea = destinationImageWidth * destinationImageHeight;
        int destinationImageR[] = new int [destinationImageArea];
        int destinationImageG[] = new int [destinationImageArea];
        int destinationImageB[] = new int [destinationImageArea];
        int destinationImagePixels[] = new int [destinationImageArea];
        destinationImage.getRGB(0, 0, destinationImageWidth, destinationImageHeight, destinationImagePixels, 0, destinationImageWidth);
        for (int i = 0; i < destinationImageArea; i++) {
            int pixel = destinationImagePixels[i];
            destinationImageR[i] = 0xff & (pixel >> 16);
            destinationImageG[i] = 0xff & (pixel >> 8);
            destinationImageB[i] = 0xff & (pixel);
        }
        //
        int arrayOfFragmentPoints[] = new int [fragmentWidth * fragmentHeight * 2];
        int counter = 0;
        for (int _y = 0; _y < fragmentHeight; _y++)
        {
            for (int _x = 0; _x < fragmentWidth; _x++)
            {
                arrayOfFragmentPoints[counter++] = _x + firstPointX;
                arrayOfFragmentPoints[counter++] = _y + firstPointY;
            }
        }
        //
        //-----------------------------------------------------------------------
        int resultImageR[] = calculateColorChannel(sourceImageR, sourceImageWidth, destinationImageR, destinationImageWidth, destinationImageHeight, arrayOfFragmentPoints, bindingPointX, bindingPointY);
        int resultImageG[] = calculateColorChannel(sourceImageG, sourceImageWidth, destinationImageG, destinationImageWidth, destinationImageHeight, arrayOfFragmentPoints, bindingPointX, bindingPointY);
        int resultImageB[] = calculateColorChannel(sourceImageB, sourceImageWidth, destinationImageB, destinationImageWidth, destinationImageHeight, arrayOfFragmentPoints, bindingPointX, bindingPointY);
        //-----------------------------------------------------------------------
        //
        int resultImagePixels[] = new int [destinationImageArea];
        for (int i = 0; i < destinationImageArea; i++) {
            resultImagePixels[i] = (resultImageR[i] << 16) | (resultImageG[i] << 8) | resultImageB[i];
        }
        //
        BufferedImage resultImage = new BufferedImage(destinationImageWidth, destinationImageHeight, BufferedImage.TYPE_INT_RGB);
        resultImage.setRGB(0, 0, destinationImageWidth, destinationImageHeight, resultImagePixels, 0, destinationImageWidth);
        return resultImage;
    }
    
    /**
     * Функция реализует алгоритм бесшовного копирования для одного цветового канала.
     * Применима также для изображений в градациях серого (256 уровней яркости).
     * 
     * @param sourceImage - одномерный массив пикселей изображения-источника;
     * @param sourceWidth - ширина изображения-источника;
     * @param destinationImage - одномерный массив пикселей изображения-приемника;
     * @param destinationWidth - ширина изображения-приемника;
     * @param destinationHeight - высота изображения-приемника;
     * @param arrayOfFragmentPoints - массив пар координат (x;y) копируемого фрагмента в изображении-источнике;
     * @param bindingPointX - x-координата точки привязки в изображении-приемнике;
     * @param bindingPointY - y-координата точки привязки в изображении-приемнике.
     * @return Одномерный массив пикселей изображения-результата типа int[].
     */
    private static int[] calculateColorChannel(int sourceImage[],
                                               int sourceWidth,
                                               int destinationImage[],
                                               int destinationWidth,
                                               int destinationHeight,
                                               int arrayOfFragmentPoints[],
                                               int bindingPointX,
                                               int bindingPointY) {
        //
        // Построение СЛАУ.
        //
        
        int countOfPoints = arrayOfFragmentPoints.length / 2;
        int[] leftPartCounters = new int [countOfPoints];

        int[] leftPartPositions = new int [countOfPoints << 2];
        int positionsCounter = 0;

        float[] rightPartKoeffitients = new float [countOfPoints];

        boolean[] pointStatus = new boolean [destinationWidth * destinationHeight];
        for (int i = 0; i < destinationWidth * destinationHeight; i++) pointStatus[i] = true;

        int[] arrayOfDestinationPoints = new int [countOfPoints << 1];

        // Заполняем два последних массива.
        int firstPointX = arrayOfFragmentPoints[0];
        int firstPointY = arrayOfFragmentPoints[1];
        for (int i = 0; i < countOfPoints << 1; ) {
            int x = arrayOfFragmentPoints[i] - firstPointX + bindingPointX;
            arrayOfDestinationPoints[i++] = x;
            int y = arrayOfFragmentPoints[i] - firstPointY + bindingPointY;
            arrayOfDestinationPoints[i++] = y;
            pointStatus[y * destinationWidth + x] = false;
        }

        //
        // Main cycle.
        //

        int tempCounter = 0;
        for (int i = 0; i < countOfPoints; i++) {
            int sx = arrayOfFragmentPoints[tempCounter];
            int dx = arrayOfDestinationPoints[tempCounter++];
            int sy = arrayOfFragmentPoints[tempCounter];
            int dy = arrayOfDestinationPoints[tempCounter++];
            // Расчитываем правую часть.
            rightPartKoeffitients[i] = (sourceImage[sy * sourceWidth + sx] << 2) -
                                       sourceImage[sy * sourceWidth + sx - 1] -
                                       sourceImage[sy * sourceWidth + sx + 1] -
                                       sourceImage[(sy - 1) * sourceWidth + sx] -
                                       sourceImage[(sy + 1) * sourceWidth + sx];
            // Анализируем каждое слагаемое левой части.
            int leftPartCounter = 0;
            // (1) (x-1; y)
            if (pointStatus[dy * destinationWidth + dx - 1]) {
                rightPartKoeffitients[i] += destinationImage[dy * destinationWidth + dx - 1];
            } else {
                leftPartPositions[positionsCounter++] = i - 1;
                leftPartCounter++;
            }
            // (2) (x+1; y)
            if (pointStatus[dy * destinationWidth + dx + 1]) {
                rightPartKoeffitients[i] += destinationImage[dy * destinationWidth + dx + 1];
            } else {
                leftPartPositions[positionsCounter++] = i + 1;
                leftPartCounter++;
            }
            // (3) (x; y-1)
            if (pointStatus[(dy - 1) * destinationWidth + dx]) {
                rightPartKoeffitients[i] += destinationImage[(dy - 1) * destinationWidth + dx];
            } else {
                int pos = i - 1;
                for (; pos >= 0; pos--) {
                    if (arrayOfDestinationPoints[(pos << 1)] == dx) break;
                }
                leftPartPositions[positionsCounter++] = pos;
                leftPartCounter++;
            }
            // (4) (x; y+1)
            if (pointStatus[(dy + 1) * destinationWidth + dx]) {
                rightPartKoeffitients[i] += destinationImage[(dy + 1) * destinationWidth + dx];
            } else {
                int pos = i + 1;
                for (; pos < countOfPoints; pos++) {
                    if (arrayOfDestinationPoints[(pos << 1)] == dx) break;
                }
                leftPartPositions[positionsCounter++] = pos;
                leftPartCounter++;
            }
            // Сохраняем количество слагаемых с коэффициентом (-1) в левой части.
            leftPartCounters[i] = leftPartCounter;
        }

        //
        // Решение СЛАУ.
        //

        float[] solution = solveHestenesStiefel(countOfPoints, leftPartCounters, leftPartPositions, rightPartKoeffitients);

        //
        // Сохраняем результат в изображение-приемник.
        //

        for (int i = 0; i < countOfPoints; i++) {
            int x = arrayOfDestinationPoints[i*2];
            int y = arrayOfDestinationPoints[i*2+1];
            int pixel = Math.round(solution[i]);
            if (pixel < 0) pixel = 0; else if (pixel > 255) pixel = 255;
            destinationImage[y * destinationWidth + x] = pixel;
        }

        return destinationImage;
    }

    /**
     * Функция решения СЛАУ по оптимизированному методу Хестенса-Штифеля.
     * 
     * @param dimension - размерность матрицы;
     * @param leftPartCounters - массив, хранящий сколько в каждой строке матрицы коэффициентов (-1);
     * @param leftPartPositions - массив подряд идущих позиций коэффициентов (-1) в строках матрицы;
     * @param rightPartKoeffitients - вектор значений правой части СЛАУ.
     * @return Вектор решения СЛАУ типа float[].
     */
    private static float[] solveHestenesStiefel(int dimension,
                                                int[] leftPartCounters,
                                                int[] leftPartPositions,
                                                float[] rightPartKoeffitients) {
        int k = 0;
        float[] x_previous = new float [dimension];
        float[] r_previous = new float [dimension];
        float[] p_previous = new float [dimension];
        float[] x_current =  new float [dimension];
        for (int i = 0; i < dimension; i++) { x_current[i] = 0; x_previous[i] = EPSILON; }
        float[] r_current = rightPartKoeffitients;
        float[] p_current =  new float [dimension];
        float rlengthPrevious = 0;
        float rlengthPrePrevious;
        float[] tempVector = new float [dimension];
        // Итерации алгоритма.
        for (;;) {
            // Проверка условия останова.
            boolean ok = true;
            for (int i = 0; i < dimension; i++) {
                float delta = r_current[i];
                if ( (delta < 0) ? (delta <= -EPSILON) : (delta >= EPSILON) ) { ok = false; break; }
            }
            if (ok) break;
            // Вычисление длины вектора r на текущей итерации.
            rlengthPrePrevious = rlengthPrevious;
            rlengthPrevious = 0;
            for (int i = 0; i < dimension; i++) rlengthPrevious += r_current[i] * r_current[i];
            // Новая итерация.
            k++;
            // Обмен векторов с текущей и предыдущей итераций.
            float[] tempPointer = null;
            tempPointer = x_previous;
            x_previous = x_current;
            x_current = tempPointer;
            tempPointer = r_previous;
            r_previous = r_current;
            r_current = tempPointer;
            tempPointer = p_previous;
            p_previous = p_current;
            p_current = tempPointer;
            //
            if (k == 1) {
                p_current = r_previous.clone();
            } else {
                float beta = rlengthPrevious / rlengthPrePrevious;
                for (int i = 0; i < dimension; i++) p_current[i] = r_previous[i] + beta * p_previous[i];
            }
            // Вычисляем alpha. Но сначала умножим матрицу на вектор.
            // Но сначала (matrix * vector).
            int positionsCounter = 0;
            float pAp = 0;  // Сразу посчитаем знаменатель выражения для вычисления alpha.
            for (int i = 0; i < dimension; i++)  // Идем по строкам матрицы...
            {
                float tempValue = p_current[i] * 4;  // На главной диагонали всегда 4.
                for (int j = 0; j < leftPartCounters[i]; j++)  // Берем только коэффициенты (-1).
                {
                    tempValue -= p_current[ leftPartPositions[positionsCounter++] ];
                }
                tempVector[i] = tempValue;
                pAp += p_current[i] * tempValue;
            }
            // Теперь, собственно, alpha.
            float alpha = rlengthPrevious / pAp;
            // Вычисляем xk.
            for (int i = 0; i < dimension; i++) x_current[i] = x_previous[i] + alpha * p_current[i];
            // Вычисляем rk.
            for (int i = 0; i < dimension; i++) r_current[i] = r_previous[i] - alpha * tempVector[i];
        }
        // Возврат решения.
        return x_current;
    }

}
